local base = _G
local math = base.math
local string = base.string
local ipairs = base.ipairs
module("gui")

TextboxG = {}
TextboxGMT = { __index = TextboxG }

base.setmetatable(TextboxG, { __index = ContainerG })

TextboxG.create = function(x, y, width, height, text)
  local self = Container(x, y, width, height, TextboxGMT)

  self.styles = get_styles('padding', 'font', 'line_spacing', 'cursor_blink')
  self.colors = get_colors('font', 'background', 'highlight', 'font_highlight', 'border')

  self.text = text or ""
  self.old_text = nil
  self.old_width = 0
  self.old_height = 0
  self.lines = {}
  self.offset = 0
  self.selection = 0
  self.cursor = 0
  self.blink = 0
  
  self.scrollbar = Scrollbar(width - 12, 0, 12, height, 1)
  self.scrollbar.node.depth = -1
  self:add_child(self.scrollbar)
  self.scrollbar.slider.on_change = function(slider, value)
    self:set_offset(value - 1)
  end
  
  self.readonly = false

  return self
end

TextboxG.destroy = function(self)
  self.styles = nil
  self.colors = nil

  self.text = nil
  self.old_text = nil
  self.old_width = nil
  self.old_height = nil
  self.lines = nil
  self.offset = nil
  self.selection = nil
  self.cursor = nil
  self.blink = nil
  
  self.readonly = nil

  self.scrollbar = nil
  ContainerG.destroy(self)
end

-- generate a table with the wrapped text
TextboxG.generate = function(self)
  local pad = self.styles.padding
  local line = self.styles.line_spacing
  
  local w = self.width - pad * 2 - 12
  -- todo: inefficient because it regenerates lines that have not changed
  self.lines = self.styles.font:wrap(self.text, w)

  local h = self.height - pad * 2
  local rows = math.floor(h/line)
  local scroll = self.scrollbar
  scroll:set_position(self.width - 12, 0)
  scroll:set_size(12, self.height)
  scroll.slider.segments = #self.lines - rows + 1

  self:update_offset()
end

-- set cursor position
TextboxG.set_cursor = function(self, index)
  -- clamp index value
  local len = string.len(self.text)
  index = math.max(index, 0)
  index = math.min(index, len)
  self.cursor = index
  self.blink = 0
end

-- returns the line number and string index at the beginning of the line
TextboxG.index_to_line = function(self, c)
  -- find the line number
  local i = 0
  for k, v in ipairs(self.lines) do
    local len = string.len(v)
    local i2 = i + len
    if c >= i and c < i2 then
      return k, i
    end
    i = i2
  end
  -- hack time: this has to be refactored
  -- the problem is that wrap doesn't return the last
  -- blank line if it the next-to-last line ends with '\n'
  if c > 0 and #self.lines > 0 then
    local k = #self.lines
    local last = self.lines[k]
    local len = string.len(last)
    local b = string.byte(self.text, c)
    local char = string.char(b)
    if char == '\n' or char == '\r' then
      return k + 1, string.len(self.text)
    end
    return k, string.len(self.text) - len
  end
  return 1, 0
end

TextboxG.update_offset = function(self)
  --shift offset to keep the cursor on-screen
  local c = self.cursor + self.selection

  local li = self:index_to_line(c)
  self.offset = math.min(self.offset, li - 1)

  local h = self.height - self.styles.padding * 2
  local l = self.styles.line_spacing
  local rows = math.floor(h/l)
  self.offset = math.max(self.offset, li - rows)
  
  self.scrollbar.slider.value = self.offset + 1
end

TextboxG.set_offset = function(self, o)
  local h = self.height - self.styles.padding * 2
  local line = self.styles.line_spacing
  local rows = math.floor(h/line)
  o = math.min(o, #self.lines - rows)
  o = math.max(o, 0)
  self.offset = o
end

-- converts string index to position in pixels
TextboxG.get_index_position = function(self, index)
  -- find the y value
  local li, i = self:index_to_line(index)
  -- find the x value
  local line = self.styles.line_spacing
  local pad = self.styles.padding
  local x = pad
  local y = -li*line - pad
  -- todo: should be refactored
  if self.lines[li] then
    local sz = string.sub(self.lines[li], 1, (index - i))
    x = x + self.styles.font:get_width(sz)
  end
  y = y + line*self.offset
  return x, y
end

-- converts position in pixels to string index
TextboxG.get_index = function(self, x, y)
  if #self.lines == 0 then
    return 0
  end
  local line = self.styles.line_spacing
  local pad = self.styles.padding
  x = x - pad
  y = y - pad + line*self.offset
  local l = math.ceil(y/line)
  l = math.max(l, 1)
  l = math.min(l, #self.lines)
  local index = 0
  for i = 1, l - 1 do
    index = index + string.len(self.lines[i])
  end
  local font = self.styles.font
  local w = font:get_width(self.lines[l])
  x = math.min(x, w)
  index = index + font:get_index(self.lines[l], x)
  return index
end

-- returns the start and end selection indices
TextboxG.get_selection_index = function(self)
  local s = self.cursor
  local e = self.cursor + self.selection
  if s > e then
    s, e = e, s
  end
  return s, e
end

-- set the selection size (could be negative)
TextboxG.set_selection = function(self, s)
  if self.readonly == true then
    return
  end
  -- clamp selection value
  local len = string.len(self.text)
  s = math.max(s, - self.cursor)
  s = math.min(s, len - self.cursor)
  -- assign
  self.selection = s
end

TextboxG.select_all = function(self)
  --self.selection = string.len(self.text)
  self:set_cursor(0)
  self:set_selection(string.len(self.text))
end

-- scroll using the mouse wheel
TextboxG.wheel_move = function(self, z)
  self.scrollbar.slider:wheel_move(z)
end

-- handle mouse press
TextboxG.mouse_press = function(self, button, x, y)
  ContainerG.mouse_press(self, button, x, y)
  if self.focus then
    return
  end
  local index = self:get_index(x, y)
  self.selection = 0
  self:set_cursor(index)
end

TextboxG.dragging = function(self, button, x, y)
  if self.focus then
    ContainerG.dragging(self, button, x, y)
    return
  end
  if self.readonly == true then
    return
  end
  self.selection = self:get_index(x, y) - self.cursor
end

TextboxG.write = function(self, c)
  if self.readonly == true then
    return
  end
  -- overwrite old selection
  local s, e = self:get_selection_index()
  local pre = string.sub(self.text, 1, s)
  local post = string.sub(self.text, e + 1)
  self.text = pre .. c .. post
  -- update the cursor
  self.selection = 0
  self:set_cursor(s + string.len(c))
  -- todo?
  --self:update_offset()
  --self:on_change(self.text)
  self:generate()
end

TextboxG.key_command = function(self, key, ctrl, shift)
  --self:generate()

  local h = self.height - self.styles.padding * 2
  local l = self.styles.line_spacing
  local rows = math.floor(h/l)
  local max_offset = #self.lines - rows
  if key == base.KEY_BACK or key == base.KEY_DEL then
    if self.selection == 0 then
      local dir = 1
      if key == base.KEY_BACK then
        dir = -dir
      end
      if ctrl == true then
        -- delete whole word
        dir = string.find_break(self.text, self.cursor + 1, dir < 0)
      end
      self:set_selection(self.selection + dir)
    end
    self:write("")
  elseif key == base.KEY_LEFT or key == base.KEY_RIGHT then
    local dir = 1
    if key == base.KEY_LEFT then
      dir = -dir
    end
    if ctrl == true then
      -- skip whole word
      dir = string.find_break(self.text, self.cursor + self.selection + 1, dir < 0)
    end
    if shift == true then
      -- shift selection
      self:set_selection(self.selection + dir)
    else
      -- move cursor
      if self.selection ~= 0 then
        self.cursor = self.cursor + self.selection
      end
      self.selection = 0
      self:set_cursor(self.cursor + dir)
    end
    self:update_offset()
  elseif key == base.KEY_UP or key == base.KEY_DOWN then
    local dir = 1
    if key == base.KEY_DOWN then
      dir = -dir
    end
    local x, y = self:get_index_position(self.cursor + self.selection)
    local c = self:get_index(x, -y + -dir * self.styles.line_spacing)
    if shift == true then
      -- shift selection
      dir = self.cursor + self.selection - c
      self:set_selection(self.selection - dir)
    else
      self.selection = 0
      self:set_cursor(c)
    end
    self:update_offset()
  elseif key == base.KEY_HOME or key == base.KEY_PGUP or key == base.KEY_END or key == base.KEY_PGDOWN then
    local i = 0
    if key == base.KEY_END or key == base.KEY_PGDOWN then
      i = max_offset
    end
    self:set_offset(i)
    --self.scrollbar.slider.value = self.offset + 1
  elseif key == base.KEY_ENTER then
    self:write("\n")
  elseif key == base.KEY_A and ctrl == true then
    self:select_all()
  end
end

TextboxG.redraw = function(self, dt)
  -- regenerate lines (if dimensions or text has changed)
  self.scrollbar:redraw(dt)

  local c = self.sprite.canvas
  c:clear()

  local alpha = 0.5
  if self.is_focused == true then
    alpha = 1
  elseif self.is_hovered == true then
    alpha = 0.75
  end

  -- draw background rectangle
  local l, t, r, b = 0, -self.height, self.width, 0
  c:rect(l, t, r, b)
  c:set_fill_style(self.colors.background, 1)
  c:fill()
  c:rect(l - 1, t, r, b + 1)
  c:set_line_style(1, self.colors.border, alpha)
  c:stroke()

  -- write the text
  local pad = self.styles.padding
  local l = self.styles.line_spacing
  local h = self.height - pad * 2
  local rows = math.floor(h/l)
  
  local s = self.offset + 1
  local e = math.min(#self.lines, s + rows - 1)
  local bs = l - self.styles.font:get_size()
  for i = s, e do
    c:move_to(pad, -(i - s + 1) * l - pad + bs)
    c:set_font(self.styles.font, self.colors.font, 1)
    c:write(self.lines[i])
  end

  if self.selection == 0 then
    if self.readonly ~= true and self.is_focused == true then
      self.blink = self.blink + dt
      local t = math.floor(self.blink/self.styles.cursor_blink)
      if t % 2 == 0 then
        -- draw the cursor
        local x, y = self:get_index_position(self.cursor)
        if y < -pad and y > (rows + 1) * -l then
          c:move_to(x, y)
          c:line_to(x, y + l)
          c:set_line_style(1, self.colors.font, 1)
          c:stroke()
        end
      end
    end
  else
    -- draw the highlighted selection
    local font = self.styles.font
    local s, e = self:get_selection_index()
    local i = 0
    for k, v in ipairs(self.lines) do
      local i2 = i + string.len(v)
      if k > self.offset and k <= self.offset + rows then
        if (i >= e or i2 <= s) == false then
          local yo = self.offset * l
          local s2 = i
          if i2 > s and i < s then
            s2 = s
          end
          local e2 = i2
          if i2 > e and i < e then
            e2 = e
          end
          local sz = string.sub(v, 1, s2 - i)
          local w = font:get_width(sz)
          local sz2 = string.sub(v, s2 - i + 1, e2 - i)
          local w2 = font:get_width(sz2)
          --w2 = math.max(w2, 5)
          local x = w + pad
          local y = -k * l - pad + yo
          c:rect(x, y, x + w2, y + l)
          c:set_fill_style(self.colors.highlight, 1)
          c:fill()
          c:move_to(w + pad, -k * l - pad + yo + bs)
          c:set_font(font, self.colors.font_highlight, 1)
          c:write(sz2)
        end
      end
      
      i = i2
    end
  end
end

TextboxG.update = function(self, dt)
  -- regenerate lines (if dimensions or text has changed)
  -- don't generate if the textbox hasn't changed
  if self.text ~= self.old_text or self.old_width ~= self.width or self.old_height ~= self.height then
    self:generate()
    self.old_width = self.width
    self.old_height = self.height
    self.old_text = self.text
  end
  ContainerG.update(self, dt)
  -- todo: don't redraw all the time
  self:redraw(dt)
end

Textbox = TextboxG.create